<?php
class MongoModel {
	
	var $table;
	
	var $name = '';
	
	var $db_name = null;
	
	private static $_static_models = array();
	/**
	 * @return MongoModel
	 */
	public static function model(){
		if(version_compare(PHP_VERSION,'5.3',">")){
			$className = get_called_class();
		}else{
			throw new CException('MongoModel::model() 必须在php 5.3及以上版本才支持!');
		}
		if(isset(self::$_static_models[$className])){
			return self::$_static_models[$className];
		}
		$mod = new $className;
		self::$_static_models[$className] = $mod;
		return $mod;
	}
	
	public function delete_by_key($key, $options=null){
		if($options == null)$options = array();
		$this->delete_by_attr(array('_id'=>$key), $options);
	}
	
	public function delete_by_attr($attr, $options=null){
		if($options == null)$options = array();
		$collection = $this->get_collection();
		$collection->remove($attr, $options);
	}
	
	/**
	 * 删除符合条件的记录的某些字段
	 * @param unknown_type $condition
	 * @param unknown_type $columns
	 */
	public function delete_column($condition, $columns=null, $options=null){
		if($options==null)
			$options = array();
		$collection = $this->get_collection();
		$columns = $this->format_columns($columns);
		$collection->update($condition, array('$unset' => $columns), $options);
	}
	
	/**
	 * 删除某个文档的某些字段
	 * @param unknown_type $key
	 * @param unknown_type $columns
	 */
	public function delete_column_by_key($key, $columns=null){
		self::delete_column(array('_id'=>$key), $columns);
	}
	
	/**
	 * 插入记录
	 * @param array $docs
	 */
	public function insert($docs){
		if(!isset($docs['_id'])){
			$docs['_id'] = CommonHelper::gen_uuid();
		}
		$docs = self::clear_null($docs);
		$collection = $this->get_collection();
		$collection->insert($docs);
	}
	
	/**
	 * 向某个文档的某个字段加$value
	 * @param unknown_type $key 文档ID
	 * @param unknown_type $columns 文档字段
	 * @param unknown_type $value 增加值
	 */
	public function inc($key, $columns, $value=1){
		$condition = array('_id' => $key);
		return $this->inc_by_attrs($condition, $columns, $value);
	}
	
	public function inc_by_attrs($condition, $columns, $value=1){
		if(empty($columns))return;
		$collection = $this->get_collection();
		if(is_string($columns)){
			$collection->update($condition, array('$inc' => array($columns => $value)));
		}elseif(is_array($columns)){
			$keys = array_keys($columns);
			if(is_int($keys[0])){
				//如果是数组是键值是数字，即是：array('count', 'comment_count')这类
				$cs = array();
				foreach ($columns as $c){
					$cs[$c] = $value;
				}
			}else{
				//如果是array('count'=>1, 'comment_count'=>3)这类
				$cs = $columns;
			}
			$collection->update($condition, array('$inc' => $cs));
		}
	}
	
	/**
	 * 查询此Key的文档是否存在
	 * @param string $key
	 */
	public function exists_key($key){
		$row = $this->get_by_key($key, '_id');
		return !empty($row);
	}
	
	/**
	 * 判断符合查询条件的文档是否存在
	 * @param array $condition
	 */
	public function exists_attrs($condition){
		$row = $this->get_by_attrs($condition);
		return !empty($row);
	}
	
	/**
	 * 判断符合某条件的记录中，某列是否存在
	 * @param unknown_type $condition
	 * @param string $column
	 */
	public function exists_column($condition, $column){
		$doc = self::get_by_attrs($condition, $column);
		if(empty($doc))
			return false;
		$cs = explode('.', $column);
		foreach ($cs as $c){
			if(isset($doc[$c])){
				$doc = $doc[$c];
			}else{
				return false;
			}
		}
		return true;
	}
	
	/**
	 * 判断某文档内，某列是否存在
	 * @param unknown_type $key
	 * @param unknown_type $column
	 */
	public function exists_column_by_key($key, $column){
		return self::exists_column(array('_id'=>$key), $column);
	}
	
	/**
	 * 通过Key值，修改文档值
	 * @param unknown_type $key
	 * @param unknown_type $attr
	 */
	public function update_by_key($key, $attr){
		$collection = $this->get_collection();
		if(isset($attr['_id'])){
			unset($attr['_id']);
		}
		$attr = self::clear_null($attr, true);
		$collection->update(array('_id' => $key), array('$set' => $attr));
	}

	/**
	 * 向文档的数组里面推一条记录
	 * @param $key 文档的_id
	 * @param $path 数组所有的路径，比如：用户里面的：'profile.emails'
	 * @param $data 要推进去的数据
	 * @param $return_index
	 */
	public function push($key, $path, $data, $return_index=false){
		$data = self::clear_null($data, true);
		$collection = $this->get_collection();
		$random = rand(100000, 999999);
		if($return_index){
			//需要返回插入后的位置
			$options = array('safe' => true);
			$data['auto_marker'] = $random;//用于标识位置
		}else{
			$options = array();
		}
		$collection->update(array('_id' => $key), array('$push' => array($path => $data)), $options);
		$column = preg_replace('/\.\d+\./i', '.', $path);//取消中间的索引号，以获取到对应的列
		$docs = self::get_by_attrs(array('_id' => $key, $path . '.auto_marker' => $random), $column . '.auto_marker');
		$data = self::get_array_value($docs, $path);
		if(empty($data))return -1;
		foreach($data as $index => $doc){
			if($doc['auto_marker'] == $random){
				//删除此属性
				self::delete_column_by_key($key, $path . '.' . $index . '.auto_marker');
				return $index;
			}
		}
		return -1;
	}
	
	/**
	 * 通过数组路径直接访问某个元素
	 * @param unknown_type $array_data 数组 比如：user = array(
	 * 	'profile' => array(
	 * 			'age' => 28,
	 * 			'name' => 'yyy'
	 * 		)
	 * );
	 * @param unknown_type $path 元素路径，多元使用.分隔，比如：profile.age
	 */
	private function get_array_value($array_data, $path){
		if(empty($array_data))return null;
		$node_keys = explode('.', $path);
		foreach ($node_keys as $key){
			if(preg_match('/^\d+$/i', $key)){
				$key = intval($key);
			}
			if(isset($array_data[$key])){
				$array_data = $array_data[$key];
			}else{
				return null;
			}
		}
		return $array_data;
	}
	
	/**
	 * 通过给定的条件，更新符合条件的文档值
	 * @param array $conditions 查询条件，与find/get中的查询条件一致
	 * @param array $attr
	 * @param array $options 更新事件的参数 
	 * 		multiple:false，表示第一个参数匹配多条记录时，会全部更新，否则只更新第一条，默认是false，MongoDB官方文档里是用multi，但PHP的驱动multiple
	 * 		upsert:false，表示，如果第一个参数没有匹配中时，是否新创建一个文档，默认为false
	 * 		safe:false，是否更新完成后才返回，默认是false。这个参数在MongoDB文档中没有出现，可能是PHP驱动里面添加的。一些非常重要的数据更新可以使用这个参数，但注意，使用了这个参数后，会延迟很多，可以明显感觉得出来
	 */
	public function update_by_attrs($conditions, $attr, $options=null){
		if($options == null)
			$options = array('multiple'=>true);
		$attr = self::clear_null($attr, true);
		$collection = $this->get_collection();
		$collection->update($conditions, array('$set' => $attr), $options);
	}
	
	/**
	 * 原生的update
	 * @param unknown $conditions
	 * @param unknown $update
	 * @param unknown $options
	 */
	public function update($conditions, $update, $options){
		$collection = $this->get_collection();
		$collection->update($conditions, $update, $options);
	}
	
	/**
	 * 只更新一个，无论匹配多少数量
	 * @param unknown_type $conditions
	 * @param unknown_type $attr
	 */
	public function update_one($conditions, $attr){
		self::update_by_attrs($conditions, $attr, array('multiple' => false));
	}
	
	
	/**
	 * 通过Key去获取文档
	 * @param string $key
	 * @param obj $columns 返回哪些字段，可以是字段名称的字符串，多个字段使用逗号分隔
	 * 			也可以是一个数组，比如array('fieldname' => true, 'fieldname2' => true).当键值为false时，表示排除此字段
	 *			无论指定返回什么，_id字段都会返回
	 */
	public function get_by_key($key, $columns=null){
		if(empty($key))
			return null;
		
		$columns = $this->format_columns($columns);
		
		$collection = $this->get_collection ();

		return $collection->findOne(array('_id' => $key), $columns);
	}
	
	/**
	 * 通过属性去取一个文档，如果多个文档符合条件，将返回第一个结果
	 * @param unknown_type $condition
	 * @param unknown_type $columns
	 */
	public function get_by_attrs($condition, $columns=null, $skip=null, $limit=null, $sort=null){
		$columns = $this->format_columns($columns);
		$collection = $this->get_collection();
		$docs = $collection->find($condition, $columns);
		if(!empty($docs) && count($docs)>0){
			foreach($docs as $doc){
				return $doc;
			}
		}
		return null;
	}
	
	public function count($condition=null){
		if($condition==null)
			$condition = array();
		$collection = $this->get_collection();
		return $collection->count($condition);
	}
	
	public function find($condition=null, $columns=null, $skip=null, $limit=null, $sort=null){
		if($condition == null)
			$condition = array();
		$collection = $this->get_collection();
		$columns = $this->format_columns($columns);
		$docs = $collection->find($condition, $columns);
		if($sort!=null && count($sort)>0){
			//排序array('field'=>-1, 'field2'=>1)，-1为降序 1为升序
			$docs = $docs->sort($sort);
		}
		if($skip!=null){
			//如果有设置$skip
			$docs = $docs->skip($skip);
		}
		if($limit!=null){
			//限制获取记录数
			$docs = $docs->limit($limit);
		}
		$data = array();
		if(!empty($docs) && count($docs)>0){
			foreach($docs as $doc){
				$data[(string)$doc['_id']] = $doc;
			}
		}
		return $data;
	}
	
	
	
	/**
	 * 重新格式化返回字段格式
	 * @param unknown_type $columns
	 */
	private function format_columns($columns){
		if($columns==null){
			$columns = array();
		}elseif(is_string($columns)){
			//如果是字符串，比如：name,age,password...转换成：array('name'=>true, 'age'=>true, 'password'=>true)
			$columns = explode(',', $columns);
			$cs = array();
			foreach($columns as $column){
				$cs[trim($column)] = true;
			}
			return $cs;
		}
		return $columns;
	}
	
	private function clear_null($data, $clear_id=false){
		if(empty($data) || !is_array($data)){
			return $data;
		}
		$changed = false;
		if($clear_id && isset($data['_id'])){
			$changed = true;
			unset($data['_id']);
		}
		foreach($data as $k=>$v){
			if($v === null){
				$changed = true;
				unset($data[$k]);
			}else if(is_array($v)){
				$data[$k] = self::clear_null($v);
			}
		}
		if($changed){
			reset($data);
		}
		return $data;
	}

	/**
	 * 获取到操作数据库的表
	 */
	public function get_collection() {
		$m = new Mongo(Yii::app()->params['mongo']);
		$db_name = ($this->db_name == null) ? MONGODB_NAME : $this->db_name;
		$collection_name = $this->table;
		$db = $m->selectDB($db_name);
		return new MongoCollection($db, $collection_name);
	}
	
	/**
	 * 获取所有的模型(Model)
	 */
	public static function get_models(){
		$cache = Yii::app()->cache;
		$key = 'model_options';
		$model_options = $cache->get($key);
		$except_dirs = array('.', '..', '.svn');
		if($model_options == null){
			$model_path = ROOT_PATH . '/protected/mongodb/';
			$handle = opendir($model_path);
			while (false !== ($model_file = readdir($handle))) {
				if(in_array($model_file, $except_dirs))continue;
				if(is_dir($model_path . $model_file))continue;
				$class_name = preg_replace('/(.*)\.php/im', '$1', $model_file);
				$m = new $class_name;
				$model_options[$m->table] = $m->name;
			}
			closedir($handle);
			$cache->set($key, $model_options);
		}
		return $model_options;
	}
}

?>